package com.pervasa.demo.kitsample.impl;

import java.util.*;

import org.sensorplatform.sensors.digitalcontact.DigitalContactSensor;
import org.sensorplatform.sensors.humidity.HumiditySensor;
import org.sensorplatform.sensors.pressure.InterlinkPressureSensor;
import org.sensorplatform.sensors.temperature.TemperatureSensor;

import com.pervasa.atlas.dev.service.*;
import com.pervasa.demo.kitsample.impl.AtlasReactiveEngine.OptimizationMode;


public class CacheManager implements Runnable {
	
	/**
	 * If the number of pushed data is bigger than this, pull mode will be considered. 
	 */
	public static int THRESHOLD_COUNT_TO_PULL = 5;
	
	/**
	 * If the standard deviation is less or equal than this,
	 * pull mode will be considered.  
	 */
	public static double THRESHOLD_SD_TO_PULL = 0;
	
	/**
	 * If the interval is less than this value times the push interval, push mode will be considered. 
	 */
	public static double THRESHOLD_INTERVALRATIO_TO_PUSH = 1.5;
	
	/**
	 * Maximum interval in milliseconds between two PULL request/reply.
	 */
	public static long MAX_PULL_INTERVAL = 60000;
	
	/**
	 * Each sensor has a mode.
	 * 
	 * ALWAYS_PUSH: The sensor will always push data.
	 * PUSH/PULL: CacheManager will manage the sensor's mode.
	 * UNKNOWN: Initial value.
	 */
	public enum MODE
	{
		ALWAYS_PUSH, PUSH, PULL, UNKNOWN
	}
	
	
	/**
	 * This is a Data structure to store current mode and last requested timestamp.
	 */
	public class SensorInfo
	{
		public SensorInfo()
		{
			sensorId = null;
			//value = 0;
			mode = MODE.UNKNOWN;
			lastRequested = null;
			interval = 2000;
			recentPushInterval = 2000;
			dev = null;
		}

		public String sensorId;
		//public double value;
		public MODE mode;
		public AtlasService dev;
		public Date lastRequested;
		public long interval;	// Milliseconds
		public long recentPushInterval;
	}
	
	
	
	private AtlasReactiveEngine client;
	private Map<String,SensorInfo> sensorInfoMap;
	private Thread updateThread;
	private DataHistoryManager historyManager;
	
	
	
	
	/**
	 * Constructor
	 * 
	 *  @param client		AtlasClient object. Normally it is the object that creates this instance.
	 *  @param historyManager	HistoryManager object to access database.
	 */
	public CacheManager(AtlasReactiveEngine client, DataHistoryManager historyManager)
	{
		this.client =  client;
		this.historyManager = historyManager;
		
		// Initialize Sensor Map
		sensorInfoMap = Collections.synchronizedMap(new HashMap<String,SensorInfo>());
		
		// Create the Cache Invalidation thread
		updateThread = new Thread(this);
		updateThread.start();
	}


	
	/**
	 * This is called when adding a sensor
	 * 
	 * @param sensorId	Sensor's ID
	 */
	public synchronized void onSensorAdded(String sensorId, AtlasService dev)
	{
		// if already exist, ignore it.
		if (sensorInfoMap.get(sensorId) != null)
		{
			return;
		}
		
		// Start PUSH mode
		SensorInfo si = new SensorInfo();
		si.sensorId = sensorId;
		si.mode = CacheManager.MODE.PUSH;
		si.dev = dev;
		sensorInfoMap.put(sensorId, si);
		
		// Register the device on sensorInfoMap
		subscribeSensor(sensorId);
	}
	
	
	/**
	 * This is called when removing a sensor
	 * 
	 * @param sensorId	Sensor's ID
	 */
	public synchronized void onSensorRemoved(String sensorId)
	{
		unsubscribeSensor(sensorId);
		sensorInfoMap.remove(sensorId);
	}
	
	
	/**
	 * This is called when receiving data from sensors
	 * 
	 * @param sensorId
	 * @param value
	 */
	public synchronized void onDataReceived(String sensorId)
	{
		// Check the current mode of the sensor
		SensorInfo si = sensorInfoMap.get(sensorId);
		if (si==null)
		{
			log("Warning: data received, but no such sensor on the list.");
			return;
		}
		
		switch(si.mode)
		{
		// if state == ALWAYS_PUSH, simply exit the method.
		case ALWAYS_PUSH:
			break;
		
		// if state == PUSH
		case PUSH:
			// if # of pushed values < THRESHOLD_TO_PULL, simple exit
			if (getRecentPushCount(sensorId) < THRESHOLD_COUNT_TO_PULL)
			{
				return;
			}
			// if # of pushed values => THRESHOLD_COUNT_TO_PULL & SD <= THRESHOLD_SD_TO_PULL,  change to the PULL mode.
			if (getRecentPushCount(sensorId) >= THRESHOLD_COUNT_TO_PULL
					&& getRecentPushSD(sensorId) <= THRESHOLD_SD_TO_PULL)
			{
				// Stop subscribing
				//unsubscribeSensor(sensorId);	// setMode() method will be responsible to do this. 
				double x = getRecentPushSD(sensorId);
				
				// Begin with the PUSH duration * 2
				si.recentPushInterval = getIntervalInMilliseconds(sensorId); 
				si.interval = si.recentPushInterval * 2;
				
				setMode(sensorId, MODE.PULL);
			}
			break;

		// if state == PULL
		case PULL:
			double variation = getLatestVariation(sensorId);
			/*
			// if |Currentvalue-Previousvalue|/Previousvalue >= 20%, set to the PUSH mode
			if (variation >= 0.2)
			{
				setMode(sensorId, MODE.PUSH);
			}
			// else if state == PULL & |C-P|/P >= 10%, shrink the PULL interval
			else if (variation >= 0.1)
			{
				if (!changePullIntervalByRatio(sensorId, 0.5))
				{
					// This means that the new interval is less than MIN_PULL_INTERVAL
				}
			}
			// if state == PULL & |C-P|/P < 10%, "consider" to extend the PULL duration
			else
			{
				changePullIntervalByMilliseconds(sensorId, getPushIntervalInMilliseconds(sensorId));
			}
			*/
			if (variation > 0)
			{
				if (si.interval*0.5 < si.recentPushInterval*THRESHOLD_INTERVALRATIO_TO_PUSH)
				{
					setMode(sensorId, MODE.PUSH);
				}
				else
				{
					changePullIntervalByRatio(sensorId, 0.5);
				}
			}
			else
			{
				changePullIntervalByMilliseconds(sensorId, si.recentPushInterval);
			}
			break;
			
		case UNKNOWN:
			log("Warning: SensorInfo was not initialized.");
			break;
		}
		
	}
	
	
	
	/**
	 * Get the value of the sensor via HistoryManager.
	 * 
	 * @param sensorId
	 * @return
	 */
	public synchronized int getSensorValue(String sensorId)
	{
		ResultData rd = historyManager.getRecentSensorValue(sensorId);
		return rd.getRecentValue();
	}
	
	
	/**
	 * Set the sensor PUSH always. Note that once the sensor falls in the permanent mode, it cannot change the mode.
	 * 
	 * @param sensorId
	 */
	public synchronized void setPermanentPush(String sensorId)
	{
		// Set to the ALWAYS_PUSH mode.
		setMode(sensorId, CacheManager.MODE.ALWAYS_PUSH);
		return;
	}
	
	
	protected void setMode(String sensorId, CacheManager.MODE mode)
	{
		SensorInfo si = sensorInfoMap.get(sensorId);
		if (si == null)
		{
			log("Warning: sensor does not exist");
			return;
		}
		
		// If the current mode is no-optimization mode, stop changing the mode!
		if (client.getOptMode() == OptimizationMode.NOOPT)
		{
			return;
		}
		
		switch (si.mode)
		{
		case ALWAYS_PUSH:
			log("Warning: You are trying to modify an ALWAYS_PUSHing sensor");
			break;
			
		case PUSH:
			if (mode == CacheManager.MODE.PULL)
			{
				log("Transition to PULL at sensor " + sensorId);
				client.setEventMode(sensorId, mode);	// Notify to Reactive Engine
				historyManager.setMode(sensorId, CurrentMode.MODE_PULL);	// Notify to History Manager
				unsubscribeSensor(sensorId);
				si.mode = mode;
				si.lastRequested = new Date();
			}
			break;
			
		case PULL:
			if (mode == CacheManager.MODE.ALWAYS_PUSH || mode == CacheManager.MODE.PUSH)
			{
				log("Transition to PUSH at sensor " + sensorId);
				client.setEventMode(sensorId, mode);	// Notify to Reactive Engine
				historyManager.setMode(sensorId, CurrentMode.MODE_PUSH);	// Notify to History Manager
				subscribeSensor(sensorId);
				si.mode = mode;
			}
			break;
		}		
		return;
	}
	
	
	public CacheManager.MODE getMode(String sensorId)
	{
		SensorInfo si = sensorInfoMap.get(sensorId);
		if (si == null) return CacheManager.MODE.UNKNOWN;
		
		return si.mode;
	}
	
	
	/**
	 * FIXME: When does Reactive Engine access this methid???
	 * 
	 * @param SensorId
	 * @param from
	 * @param to
	 * @return
	 */
	public boolean isValueWithinRange(String SensorId, double from, double to)
	{
		return false;
	}
	
	
	
	
	
	/******************************************************************
	 * The following methods are used only in this class.
	 ******************************************************************/
	
	/**
	 * Begin to subscribe sensor's data. The sensor will periodically send it.
	 */
	private void subscribeSensor(String sensorId)
	{
		SensorInfo si = sensorInfoMap.get(sensorId);
		if (si == null) return;
		AtlasService dev = si.dev;
		
		// Register the device on sensorInfoMap
		if (dev instanceof InterlinkPressureSensor) {
			((InterlinkPressureSensor) dev).subscribeToPressureData(client);
		}
		// if the digital contact sensor service comes online, grab a reference to it,
		//   subscribe to its data stream, and update the availability icon
		else if (dev instanceof DigitalContactSensor) {
			((DigitalContactSensor) dev).subscribeToContactData(client);
		}
		// Temperature sensor
		else if (dev instanceof TemperatureSensor){
			((TemperatureSensor) dev).subscribeToSensorData(client);
		}
		// Humidity sensor
		else if (dev instanceof HumiditySensor){
			((HumiditySensor) dev).subscribeToSensorData(client);
		}
		else
		{
			log("Warning: unknown sensor type." + dev.toString());
		}
	}
	
	
	/**
	 * Stop subscribing sensor's data. The sensor will not send data anymore.
	 * @param sensorId
	 */
	private void unsubscribeSensor(String sensorId)
	{
		SensorInfo si = sensorInfoMap.get(sensorId);
		if (si == null) return;
		AtlasService dev = si.dev;
		
		// Unsubscribe the sensor
		if (dev instanceof InterlinkPressureSensor) {
			((InterlinkPressureSensor) dev).unsubscribeFromPressureData(client);
		}
		else if (dev instanceof DigitalContactSensor) {
			((DigitalContactSensor) dev).unsubscribeFromContactData(client);
		}
		else if (dev instanceof TemperatureSensor){
			((TemperatureSensor) dev).unsubscribeFromSensorData(client);
		}
		else if (dev instanceof HumiditySensor){
			((HumiditySensor) dev).unsubscribeFromSensorData(client);
		}
		else
		{
			log("Warning: unknown sensor type." + dev.toString());
		}

	}
	
	
	

	/**
	 * Query a request to the sensor to send its current value.
	 * 
	 * @param sensorId
	 */
	private void requestToSensor(String sensorId)
	{
		SensorInfo si = sensorInfoMap.get(sensorId);
		if (si == null)
		{
			log("Warning: no such sensor found.");
			return;
		}
		AtlasService dev = si.dev;		
		// Each sensor has a different method to request. Thus, this is necessary.
		if (dev instanceof InterlinkPressureSensor) {
			((InterlinkPressureSensor) dev).getPressureReading(client);
		}
		else if (dev instanceof DigitalContactSensor) {
			((DigitalContactSensor) dev).getContactReading(client);
		}
		else if (dev instanceof TemperatureSensor){
			((TemperatureSensor) dev).getSensorReading(client);;
		}
		else if (dev instanceof HumiditySensor){
			((HumiditySensor) dev).getSensorReading(client);
		}
		else
		{
			log("Warning: unknown sensor type." + dev.toString());
		}
	}

	
	
	
	
	/**
	 * Shrink the PULL period. This means that the RectivityEngine will retrieve the sensor's data more frequently.
	 * 
	 * @param sensorId
	 * @param ratio
	 */
	private boolean changePullIntervalByRatio(String sensorId, double ratio)
	{
		SensorInfo si = sensorInfoMap.get(sensorId);
		if (si == null)
		{
			log("Warning: no such sensors exist.");
			return false;
		}
		
		if (si.interval*ratio > MAX_PULL_INTERVAL)
		{
			return false;
		}
		
		si.interval = (long)(si.interval*ratio);
		
		return true;
	}
	
	
	private boolean changePullIntervalByMilliseconds(String sensorId, long milliseconds)
	{
		SensorInfo si = sensorInfoMap.get(sensorId);
		if (si == null)
		{
			log("Warning: no such sensors exist.");
			return false;
		}
		
		if (si.interval+milliseconds > MAX_PULL_INTERVAL)
		{
			return false;
		}
		
		si.interval = si.interval+milliseconds;
		
		return true;
	}
	
	
	
	
	/**
	 * This method is for the update thread.
	 */
	public void run() {
		while(true)
		{
			try
			{
				Thread.sleep(getNextInterval()); // sleep for the duration
				updateExpiredSensors();
			}
			catch (InterruptedException ie) {
				log(ie.getMessage());
			}
		}
	}
	
	
	private synchronized long getNextInterval()
	{
		long next_interval = Long.MAX_VALUE;
		
		// Read the date of each sensor
		for (String key: sensorInfoMap.keySet())
		{
			SensorInfo si = sensorInfoMap.get(key);
			if (si.mode != MODE.PULL) continue;
			
			// Calculate the next time of this sensor
			Calendar c = Calendar.getInstance();
			c.setTime(si.lastRequested);
			c.add(Calendar.MILLISECOND, (int)si.interval);
			long diff = c.getTimeInMillis() - Calendar.getInstance().getTimeInMillis();
			if (diff < 0) diff = 0;
			
			if (diff < next_interval) next_interval = diff;
		}
		
		if (next_interval == Long.MAX_VALUE) next_interval = 1000;
		log("Next interval: " + next_interval);
		
		return next_interval;
	}
	
	
	
	/**
	 * Update invalidate caches. Note that this does not consider any values of sensors
	 */
	private synchronized void updateExpiredSensors()
	{
		// Read the date of each sensor
		Set keySet = sensorInfoMap.keySet();
		Iterator it = keySet.iterator();
		while(it.hasNext())
		{
			SensorInfo si = sensorInfoMap.get(it.next());
			if (si.mode != MODE.PULL) continue;
			
			// For PULL mode, if the date is out of date, send PULL request
			Calendar c = Calendar.getInstance();
			c.setTime(si.lastRequested);
			c.add(Calendar.MILLISECOND, (int)si.interval);
			if (c.compareTo(Calendar.getInstance()) <= 0)
			{
				// Send PULL request
				requestToSensor(si.sensorId);
				
				// update lastUpdated field
				si.lastRequested = new Date();
			}
		}
	}
	
	
	
	

	/**
	 * Get the # of recently pushed data 
	 *  
	 * @param sensorId
	 * @return
	 */
	private int getRecentPushCount(String sensorId)
	{
		ResultData rd = historyManager.getRecentPushCount(sensorId);
		return rd.getCountValue();
	}
	

	/**
	 * Get the SD of recently pushed data 
	 * 
	 * @param sensorId
	 * @return
	 */
	private double getRecentPushSD(String sensorId)
	{
		ResultData rd = historyManager.getStandardDeviation(sensorId, THRESHOLD_COUNT_TO_PULL, true);
		return rd.getStandardDeviation();
	}
	
	
	
	/**
	 * Get the interval among recently pushed data
	 * 
	 * @param sensorId
	 * @return
	 */
	private long getIntervalInMilliseconds(String sensorId)
	{
		return historyManager.getSensorInterval(sensorId);
	}
	
	

	/**
	 * Return the amount of variation of the latest two values.
	 * 
	 * @param sensorId
	 * @return
	 */
	private double getLatestVariation(String sensorId)
	{
		ResultData rd = historyManager.getStandardDeviation(sensorId, 2, false);
		return rd.getStandardDeviation();
	}
	
	
	
	

	private void log(String msg)
	{
		String method_name = null;
		
		try
		{
			throw new Exception();
		}
		catch(Exception e)
		{
			StackTraceElement[] ste_arr = e.getStackTrace();
			method_name = ste_arr[1].getMethodName();
		}
		
		System.out.println("CacheManager." + method_name + "(): " + msg);
	}

}
